package request

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"regexp"
	"strings"
)

// TODO make sure these are correct
var (
	maxParam         = 50
	acceptsHTMLRegex = regexp.MustCompile(`(text/html|application/xhtml\+xml)(?:,|$)`)
	acceptsXMLRegex  = regexp.MustCompile(`(application/xml|text/xml)(?:,|$)`)
	acceptsJSONRegex = regexp.MustCompile(`(application/json)(?:,|$)`)
)

type OrangeInput struct {
	request  *http.Request
	pnames   []string
	pvalues  []string
	data     map[interface{}]interface{} // store some values in this context when calling context in filter or controller.
	FormBody []byte
}

// NewInput return OrangeInput.
func NewInput(request *http.Request, maxMemory int64) *OrangeInput {
	var buf []byte
	if request.ContentLength > maxMemory {
		panic(fmt.Sprintf("request body is too large, msxMemory is %d", maxMemory))
	}
	orangeInput := &OrangeInput{
		request:  request,
		pnames:   make([]string, 0, maxParam),
		pvalues:  make([]string, 0, maxParam),
		data:     make(map[interface{}]interface{}),
		FormBody: buf,
	}

	if request.Method == "POST" && orangeInput.IsUpload() == false {
		buf, _ = ioutil.ReadAll(request.Body)
		request.Body = ioutil.NopCloser(bytes.NewBuffer(buf))
		orangeInput.FormBody = buf
	}

	return orangeInput
}

// Reset init the
func (input *OrangeInput) Reset(request *http.Request) {
	input.request = request
	input.pnames = input.pnames[:0]
	input.pvalues = input.pvalues[:0]
	input.data = nil
	input.FormBody = []byte{}
}

// Protocol returns request protocol name, such as HTTP/1.1 .
func (input *OrangeInput) Protocol() string {
	return input.request.Proto
}

// URI returns full request url with query string, fragment.
func (input *OrangeInput) URI() string {
	return input.request.RequestURI
}

// URL returns request url path (without query string, fragment).
func (input *OrangeInput) URL() string {
	return input.request.URL.Path
}

// Site returns base site url as scheme://domain type.
func (input *OrangeInput) Site() string {
	return input.Scheme() + "://" + input.Domain()
}

// Scheme returns request scheme as "http" or "https".
func (input *OrangeInput) Scheme() string {
	if scheme := input.Header("X-Forwarded-Proto"); scheme != "" {
		return scheme
	}
	if input.request.URL.Scheme != "" {
		return input.request.URL.Scheme
	}
	if input.request.TLS == nil {
		return "http"
	}
	return "https"
}

// Domain returns host name.
// Alias of Host method.
func (input *OrangeInput) Domain() string {
	return input.Host()
}

// Host returns host name.
// if no host info in request, return localhost.
func (input *OrangeInput) Host() string {
	if input.request.Host != "" {
		if hostPart, _, err := net.SplitHostPort(input.request.Host); err == nil {
			return hostPart
		}
		return input.request.Host
	}
	return "localhost"
}

// Method returns http request method.
func (input *OrangeInput) Method() string {
	return input.request.Method
}

// Is returns boolean of this request is on given method, such as Is("POST").
func (input *OrangeInput) Is(method string) bool {
	return input.Method() == method
}

// IsGet Is this a GET method request?
func (input *OrangeInput) IsGet() bool {
	return input.Is("GET")
}

// IsPost Is this a POST method request?
func (input *OrangeInput) IsPost() bool {
	return input.Is("POST")
}

// IsHead Is this a Head method request?
func (input *OrangeInput) IsHead() bool {
	return input.Is("HEAD")
}

// IsOptions Is this a OPTIONS method request?
func (input *OrangeInput) IsOptions() bool {
	return input.Is("OPTIONS")
}

// IsPut Is this a PUT method request?
func (input *OrangeInput) IsPut() bool {
	return input.Is("PUT")
}

// IsDelete Is this a DELETE method request?
func (input *OrangeInput) IsDelete() bool {
	return input.Is("DELETE")
}

// IsPatch Is this a PATCH method request?
func (input *OrangeInput) IsPatch() bool {
	return input.Is("PATCH")
}

// IsAjax returns boolean of this request is generated by ajax.
func (input *OrangeInput) IsAjax() bool {
	return input.Header("X-Requested-With") == "XMLHttpRequest"
}

// IsSecure returns boolean of this request is in https.
func (input *OrangeInput) IsSecure() bool {
	return input.Scheme() == "https"
}

// IsWebsocket returns boolean of this request is in webSocket.
func (input *OrangeInput) IsWebsocket() bool {
	return input.Header("Upgrade") == "websocket"
}

// IsUpload returns boolean of whether file uploads in this request or not..
func (input *OrangeInput) IsUpload() bool {
	return strings.Contains(input.Header("Content-Type"), "multipart/form-data")
}

// AcceptsHTML Checks if request accepts html response
func (input *OrangeInput) AcceptsHTML() bool {
	return acceptsHTMLRegex.MatchString(input.Header("Accept"))
}

// AcceptsXML Checks if request accepts xml response
func (input *OrangeInput) AcceptsXML() bool {
	return acceptsXMLRegex.MatchString(input.Header("Accept"))
}

// AcceptsJSON Checks if request accepts json response
func (input *OrangeInput) AcceptsJSON() bool {
	return acceptsJSONRegex.MatchString(input.Header("Accept"))
}

// Header returns request header item string by a given string.
// if non-existed, return empty string.
func (input *OrangeInput) Header(key string) string {
	return input.request.Header.Get(key)
}

// ParamsLen return the length of the params
func (input *OrangeInput) ParamsLen() int {
	return len(input.pnames)
}

// Param returns router param by a given key.
func (input *OrangeInput) Param(key string) string {
	for i, v := range input.pnames {
		if v == key && i <= len(input.pvalues) {
			return input.pvalues[i]
		}
	}
	return ""
}

// Params returns the map[key]value.
func (input *OrangeInput) Params() map[string]string {
	m := make(map[string]string)
	for i, v := range input.pnames {
		if i <= len(input.pvalues) {
			m[v] = input.pvalues[i]
		}
	}
	return m
}

// SetParam will set the param with key and value
func (input *OrangeInput) SetParam(key, val string) {
	// check if already exists
	for i, v := range input.pnames {
		if v == key && i <= len(input.pvalues) {
			input.pvalues[i] = val
			return
		}
	}
	input.pvalues = append(input.pvalues, val)
	input.pnames = append(input.pnames, key)
}

// ResetParams clears any of the input's Params
// This function is used to clear parameters so they may be reset between filter
// passes.
func (input *OrangeInput) ResetParams() {
	input.pnames = input.pnames[:0]
	input.pvalues = input.pvalues[:0]
}

// IP returns request client ip.
// if in proxy, return first proxy id.
// if error, return RemoteAddr.
func (input *OrangeInput) IP() string {
	ips := input.Proxy()
	if len(ips) > 0 && ips[0] != "" {
		rip, _, err := net.SplitHostPort(ips[0])
		if err != nil {
			rip = ips[0]
		}
		return rip
	}
	if ip, _, err := net.SplitHostPort(input.request.RemoteAddr); err == nil {
		return ip
	}
	return input.request.RemoteAddr
}

// Proxy returns proxy client ips slice.
func (input *OrangeInput) Proxy() []string {
	if ips := input.Header("X-Forwarded-For"); ips != "" {
		return strings.Split(ips, ",")
	}
	return []string{}
}

// Referer returns http referer header.
func (input *OrangeInput) Referer() string {
	return input.Header("Referer")
}

// Query returns input data item string by a given string.
func (input *OrangeInput) Query(key string) string {
	if val := input.Param(key); val != "" {
		return val
	}
	if input.request.Form == nil {
		input.request.ParseForm()
	}
	return input.request.Form.Get(key)
}

// Data return the implicit data in the input
func (input *OrangeInput) Data() map[interface{}]interface{} {
	if input.data == nil {
		input.data = make(map[interface{}]interface{})
	}
	return input.data
}

// GetData returns the stored data in this context.
func (input *OrangeInput) GetData(key interface{}) interface{} {
	if v, ok := input.data[key]; ok {
		return v
	}
	return nil
}

// SetData stores data with given key in this context.
// This data are only available in this context.
func (input *OrangeInput) SetData(key, val interface{}) {
	if input.data == nil {
		input.data = make(map[interface{}]interface{})
	}
	input.data[key] = val
}

// ParseFormOrMulitForm parseForm or parseMultiForm based on Content-type
func (input *OrangeInput) ParseFormOrMulitForm(maxMemory int64) error {
	// Parse the body depending on the content type.
	if strings.Contains(input.Header("Content-Type"), "multipart/form-data") {
		if err := input.request.ParseMultipartForm(maxMemory); err != nil {
			return errors.New("Error parsing request body:" + err.Error())
		}
	} else if err := input.request.ParseForm(); err != nil {
		return errors.New("Error parsing request body:" + err.Error())
	}
	return nil
}
